import {
  ASTUtils,
  isNotNullOrUndefined,
  RuleFixes,
  Selectors,
} from '@angular-eslint/utils';
import type { TSESTree } from '@typescript-eslint/utils';
import { createESLintRule } from '../utils/create-eslint-rule';

export type Options = [];
export type MessageIds =
  | 'preferOnPushComponentChangeDetection'
  | 'suggestAddChangeDetectionOnPush';
export const RULE_NAME = 'prefer-on-push-component-change-detection';

const METADATA_PROPERTY_NAME = 'changeDetection';
const STRATEGY_ON_PUSH = 'ChangeDetectionStrategy.OnPush';

export default createESLintRule<Options, MessageIds>({
  name: RULE_NAME,
  meta: {
    type: 'suggestion',
    docs: {
      description: `Ensures component's \`${METADATA_PROPERTY_NAME}\` is set to \`${STRATEGY_ON_PUSH}\``,
    },
    hasSuggestions: true,
    schema: [],
    messages: {
      preferOnPushComponentChangeDetection: `The component's \`${METADATA_PROPERTY_NAME}\` value should be set to \`${STRATEGY_ON_PUSH}\``,
      suggestAddChangeDetectionOnPush: `Add \`${STRATEGY_ON_PUSH}\``,
    },
  },
  defaultOptions: [],
  create(context) {
    const changeDetectionMetadataProperty = Selectors.metadataProperty(
      METADATA_PROPERTY_NAME,
    );
    const withoutChangeDetectionDecorator =
      `${Selectors.COMPONENT_CLASS_DECORATOR}:matches([expression.arguments.length=0], [expression.arguments.0.type='ObjectExpression']:not(:has(${changeDetectionMetadataProperty})))` as const;
    const nonChangeDetectionOnPushProperty =
      `${Selectors.COMPONENT_CLASS_DECORATOR} > CallExpression > ObjectExpression > ${changeDetectionMetadataProperty}:matches([value.type='Identifier'][value.name='undefined'], [value.object.name='ChangeDetectionStrategy'][value.property.name!='OnPush'])` as const;
    const selectors = [
      withoutChangeDetectionDecorator,
      nonChangeDetectionOnPushProperty,
    ].join(',');

    return {
      [selectors](node: TSESTree.Decorator | TSESTree.Property) {
        context.report({
          node: nodeToReport(node),
          messageId: 'preferOnPushComponentChangeDetection',
          suggest: [
            {
              messageId: 'suggestAddChangeDetectionOnPush',
              fix: (fixer) => {
                if (ASTUtils.isProperty(node)) {
                  return [
                    RuleFixes.getImportAddFix({
                      fixer,
                      importName: 'ChangeDetectionStrategy',
                      moduleName: '@angular/core',
                      node: node.parent.parent.parent!.parent!,
                    }),
                    ASTUtils.isMemberExpression(node.value)
                      ? fixer.replaceText(node.value.property, 'OnPush')
                      : fixer.replaceText(node.value, STRATEGY_ON_PUSH),
                  ].filter(isNotNullOrUndefined);
                }

                return [
                  RuleFixes.getImportAddFix({
                    fixer,
                    importName: 'ChangeDetectionStrategy',
                    moduleName: '@angular/core',
                    node: node.parent,
                  }),
                  RuleFixes.getDecoratorPropertyAddFix(
                    node,
                    fixer,
                    `${METADATA_PROPERTY_NAME}: ${STRATEGY_ON_PUSH}`,
                  ),
                ].filter(isNotNullOrUndefined);
              },
            },
          ],
        });
      },
    };
  },
});

function nodeToReport(node: TSESTree.Node) {
  if (!ASTUtils.isProperty(node)) {
    return node;
  }

  return ASTUtils.isMemberExpression(node.value)
    ? node.value.property
    : node.value;
}

export const RULE_DOCS_EXTENSION = {
  rationale: `By default, Angular's change detection checks every component on every change detection cycle, which can involve thousands of checks per second in a large application. OnPush change detection strategy is a performance optimization that tells Angular to only check a component when: (1) its input properties receive new references, (2) an event originates from the component or its children, or (3) change detection is manually triggered. This dramatically reduces the number of change detection runs, improving application performance. OnPush pairs well with immutable data patterns and Angular signals, and is considered a best practice for most components. However, you must be careful to use immutable data patterns (creating new object references when data changes) for OnPush to work correctly.`,
};
